package timbap.visualizer.impl.applet;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import oscP5.OscEventListener;
import oscP5.OscP5;
import processing.core.PApplet;
import processing.core.PConstants;
import timbap.visualizer.api.context.Configuration;
import timbap.visualizer.api.context.messaging.InterpolatingTimeCodePublisher;
import timbap.visualizer.api.context.messaging.TimeCodeListener;
import timbap.visualizer.api.context.messaging.TimeCodePublisher;
import timbap.visualizer.api.context.playlist.PlaylistManager;
import timbap.visualizer.api.exceptions.NoPlaylistSelectedException;
import timbap.visualizer.api.exceptions.PlaylistParserException;
import timbap.visualizer.api.exceptions.ReselectPlaylistException;
import timbap.visualizer.api.model.MsPinkyTimeCode;
import timbap.visualizer.api.model.TimeCode;
import timbap.visualizer.api.plugin.Plugin;
import timbap.visualizer.api.plugin.configuration.PluginManager;
import timbap.visualizer.api.plugin.container.PluginSwitch;
import timbap.visualizer.impl.context.ContextInitializationThread;
import timbap.visualizer.impl.context.OscPluginContextImpl;
import timbap.visualizer.impl.context.messaging.InterpolatingOscTimeCodePublisherImpl;
import timbap.visualizer.impl.context.playlist.ConstantPlaylistChooserImpl;
import timbap.visualizer.impl.context.playlist.ExtM3UPlaylistManagerImpl;
import timbap.visualizer.impl.context.playlist.ITunesLibraryParsingPopupPlaylistChooserImpl;

import javax.swing.ImageIcon;
import javax.swing.UIManager;
import java.awt.event.KeyEvent;
import java.io.File;
import java.io.FileFilter;
import java.io.IOException;

public class VisualizerPApplet extends PApplet {

	private static final long serialVersionUID = 1L;
    private static final Log LOG = LogFactory.getLog(VisualizerPApplet.class);
    
	private static final String TIMBAP_APPLICATION_NAME = "timbap visualizer 0.6-beta";
	private static final String TIMBAP_APPLICATION_ICON = "timbap_visualizer_icon_16x16.png";

    private static PluginManager pluginManager;
    private static TimeCodePublisher timeCodePublisher;
    private static PlaylistManager playlistManager;
    private static Configuration cfg;
	private static ContextInitializationThread initThread;
    private static OscP5 oscServer;

    private static String loadingText = "";
    private static File[] playlistFiles;
    private static int selectedPlaylistIndex = 0;
    private static long selectionChangeTime;
    private static InitState initState = InitState.UNINITIALIZED;

    private static boolean configurationChanged = false;

    public static void main(String args[]) {

        try {
            UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
        } catch (Exception e) {
            LOG.error("Could not set system look and feel " + UIManager.getSystemLookAndFeelClassName() + ": " + e.getMessage());
        }

        cfg = new Configuration();
        cfg.load();
        cfg.findUnoccupiedPorts();

        timeCodePublisher = new InterpolatingOscTimeCodePublisherImpl(MsPinkyTimeCode.class);
        OscEventListener tEventListener = (OscEventListener)timeCodePublisher;
        oscServer = new OscP5(tEventListener, cfg.getListeningPort());

        playlistManager = new ExtM3UPlaylistManagerImpl();
        if (!cfg.isUseTurntableForPlaylistSelection()) {
            playlistManager.setPlaylistChooser(new ITunesLibraryParsingPopupPlaylistChooserImpl(cfg));
        }
        
        String[] pAppletParams;
        if (cfg.isFullScreenEnabled()) {
            pAppletParams = new String[] {"--display=1", "--present", VisualizerPApplet.class.getName()};
        } else {
            pAppletParams = new String[] {VisualizerPApplet.class.getName()};
        }
        PApplet.main(pAppletParams);

    }

    @Override
    public void setup() {

        frame.setIconImage(new ImageIcon(loadBytes(TIMBAP_APPLICATION_ICON)).getImage());
        frame.setTitle(TIMBAP_APPLICATION_NAME + " (" + cfg.getListeningPort() + "," + cfg.getSendingPort() + ")");

    	size(cfg.getScreenWidth(), cfg.getScreenHeight(), PConstants.OPENGL);
    	
        noStroke();
        textFont(loadFont(cfg.getStandardFont()));
        frameRate = cfg.getMaximumFrameRate();

        invalidatePlaylistSelection();
        initPlaylistSelectionListener();
    }

    private static void invalidatePlaylistSelection() {
        initState = InitState.UNINITIALIZED;
        selectionChangeTime = System.currentTimeMillis();
        playlistFiles = new File[0];
    }

    private static void initPlaylistSelectionListener() {

        if (cfg.getDefaultPlaylistDirectory().canRead()) {
			playlistFiles = cfg.getDefaultPlaylistDirectory().listFiles(new FileFilter() {
				public boolean accept(File pathname) {
					return pathname.getName().endsWith(".m3u");
				}
			}
			);
		}
		if (playlistFiles == null || playlistFiles.length == 0) {
			LOG.error("No playlists found in directory " + cfg.getDefaultPlaylistDirectory().getAbsolutePath() + ". Quitting ...");
			System.exit(0);
		}

        timeCodePublisher.setTimeCodeListener(new TimeCodeListener() {
            public void timeCodeUpdated(TimeCode pTimeCode) {
                int tOldSelectedPlaylistIndex = selectedPlaylistIndex;
                float tAbsPosition = pTimeCode.getAbsolutePosition();
                selectedPlaylistIndex = (int) (tAbsPosition / 20) % playlistFiles.length;
                if (selectedPlaylistIndex != tOldSelectedPlaylistIndex) {
                    selectionChangeTime = System.currentTimeMillis();
                }
            }
        });
    }

    private void handleUninitializedState() {
        if (!timeCodePublisher.isTimeCodeValid()) {
            selectionChangeTime = System.currentTimeMillis();
        }
        if (!cfg.isUseTurntableForPlaylistSelection() || System.currentTimeMillis() - selectionChangeTime > 5000) {
            playlistManager.setPlaylistChooser(
                    new ConstantPlaylistChooserImpl(playlistFiles[selectedPlaylistIndex]));
            initPlaylistManager(playlistManager, cfg);
            timeCodePublisher.setTimeCodeListener(null);
            initThread = new ContextInitializationThread(
                    new OscPluginContextImpl(this, cfg, playlistManager, null, null, null),
                    timeCodePublisher, oscServer);
            initThread.start();
        } else {
            drawPlaylistSelection();
        }
    }

    enum InitState {
        UNINITIALIZED, PLAYLIST_SELECTED, INITIALIZING, READY_FOR_COMPLETION, NO_TIMECODE, COMPLETE
    }

    private void updateInitState() {

        switch (initState) {

            case UNINITIALIZED:
                if (initThread != null) {
                    initState = InitState.INITIALIZING;
                }
                break;

            case INITIALIZING:
                if (initThread.isFinished()) {
                    initState = InitState.READY_FOR_COMPLETION;
                }
                break;

            case READY_FOR_COMPLETION:
                if (pluginManager != null && timeCodePublisher != null) {
                    initState = (timeCodePublisher.hasTimeCode()) ? InitState.COMPLETE : InitState.NO_TIMECODE;
                }
                break;

            case NO_TIMECODE:
                if (timeCodePublisher.hasTimeCode() && !timeCodePublisher.isTimeCodeStale()) {
                    initState = InitState.COMPLETE;
                }
                break;

            case COMPLETE:
                if (!timeCodePublisher.hasTimeCode() || timeCodePublisher.isTimeCodeStale()) {
                    initState = InitState.NO_TIMECODE;
                }
                break;

        }
    }

    private void handleCompleteState() {
        try {
            pluginManager.getRootPlugin().draw();
        } catch (ReselectPlaylistException e) {
            LOG.warn("Playlist reselection requested. Invalidating playlist selection.");
            invalidatePlaylistSelection();
            initPlaylistSelectionListener();
            if (timeCodePublisher instanceof InterpolatingTimeCodePublisher) {
                ((InterpolatingTimeCodePublisher)timeCodePublisher).setTimeCodeInterpolation(0, 0, 1);
            }
        }
    }

    private void handleNoTimeCodeState() {
        loadingText = "Waiting for incoming time code data from audio component on port " + cfg.getListeningPort() + " ...";
        drawLoadingAnimation();
    }

    private void handleReadyForCompletionState() {
        pluginManager = initThread.getPluginManager();
        timeCodePublisher = initThread.getTimeCodePublisher();
        initThread = null;
        LOG.info("Initialization finished.");
    }

    private void handleInitializingState() {
        drawLoadingAnimation();
    }

    @Override
    public void draw() {

    	background(0);
        translate(width/2,height/2);
        rotate(PApplet.TWO_PI / 360 * cfg.getScreenRotation());
        scale(cfg.getBrowsingScaleFactor());

        updateInitState();
        switch (initState) {

            case COMPLETE: handleCompleteState(); break;

            case NO_TIMECODE:  handleNoTimeCodeState(); break;

            case UNINITIALIZED: handleUninitializedState(); break;

            case READY_FOR_COMPLETION: handleReadyForCompletionState(); break;

            case INITIALIZING: handleInitializingState(); break;

        }
    }

    private boolean lastTimeCodeValid = false;

    private void drawPlaylistSelection() {

        final boolean tTimeCodeValid = timeCodePublisher.isTimeCodeValid();
        if (tTimeCodeValid && !lastTimeCodeValid) {
            initPlaylistSelectionListener();
        }

        if (!tTimeCodeValid) {
            loadingText = "Waiting for incoming time code data from audio component on port " + cfg.getListeningPort() + " ...";
            drawLoadingAnimation();
            fill(50,30,30);
        } else {
            fill(200,150,150);
        }
        pushMatrix();
    	translate(-10,-10);
    	scale(cfg.getScreenHeight() / 400 * 0.33f);
        text("2) Choose a playlist (by moving the timecode record):", -345, -cfg.getScreenHeight() + 50, 1500, 50);

        float tSelectionTimeFractionPassed = (float) (System.currentTimeMillis() - selectionChangeTime) / 5000;
        for (int i = 0; i < playlistFiles.length; i++) {
            File tPlaylistFile = playlistFiles[i];
            float xPos = 0;
            float yPos = -cfg.getScreenHeight() + 150 + (i * 60);
            if (!tTimeCodeValid) {
                fill(50,30,30);
            } else if (i == selectedPlaylistIndex) {
                fill(255);
                text(">", -345, yPos, 50, 50);
                xPos = -100 * pow(tSelectionTimeFractionPassed, 20);
            } else {
                fill(200 * (1 - pow(tSelectionTimeFractionPassed, 3)),
                     150 * (1 - pow(tSelectionTimeFractionPassed, 3)),
                     150 * (1 - pow(tSelectionTimeFractionPassed, 3)));
            }
            text(tPlaylistFile.getName(), xPos - 200, yPos, 1000, 50);
        }
    	popMatrix();
        lastTimeCodeValid = tTimeCodeValid;
    }

    private void drawLoadingAnimation(){
    	
    	if (initThread != null && initThread.getProgressMessage() != null && !initThread.isFinished()) {
    		loadingText = initThread.getProgressMessage();
    	}
    	
    	pushMatrix();
    	fill(200,150,150);
    	translate(-10,-10);
    	scale(cfg.getScreenHeight() / 400 * 0.33f);
    	text(loadingText,
    		 100,100,
    		 1000,500);
    	popMatrix();
    	
    	for (int i=0; i < 6; i++) {
    		float value = sin((millis() % 1000) / 1000f * PI);
    		float brightness = 90 + (value * 120);
    		fill(brightness, brightness - 40, brightness - 40);
    		pushMatrix();
    		translate(value * 20 * sin(i), value * 20 * cos(i));
    		ellipse(0,0,10,10);
    		popMatrix();
    	}
    	
    }

    @Override
    public void keyPressed() {
        //TODO Find a better way to reference the pos cloud plugin
        if (keyCode == PConstants.RIGHT || keyCode == PConstants.LEFT && pluginManager != null) {
            Plugin tPosCloudSwitch = pluginManager.getPlugin("DefaultPosCloudSwitchImpl");
            if (tPosCloudSwitch instanceof PluginSwitch) {
                if (keyCode == PConstants.RIGHT) {
                    ((PluginSwitch)tPosCloudSwitch).toNext();
                } else if (keyCode == PConstants.LEFT) {
                    ((PluginSwitch)tPosCloudSwitch).toPrevious();
                }
            }
        } else if (keyCode == 'R' || keyCode == 'r') {
            if (keyEvent.getModifiers() == KeyEvent.SHIFT_MASK) {
                cfg.rotateScreen(-2);
            } else {
                cfg.rotateScreen(2);
            }
            configurationChanged = true;
        } else if (keyCode == 'W' || keyCode == 'w') {
            cfg.translatePosCloudYPos(-2);
            configurationChanged = true;
        } else if (keyCode == 'S' || keyCode == 's') {
            cfg.translatePosCloudYPos(2);
            configurationChanged = true;
        } else if (keyCode == 'A' || keyCode == 'a') {
            cfg.translatePosCloudXPos(-2);
            configurationChanged = true;
        } else if (keyCode == 'D' || keyCode == 'd') {
            cfg.translatePosCloudXPos(2);
            configurationChanged = true;
        } else if (keyCode == 'Q' || keyCode == 'q') {
            if (keyEvent.getModifiers() == KeyEvent.SHIFT_MASK) {
                cfg.rotatePosCloud(-2);
            } else {
                cfg.rotatePosCloud(2);
            }
            configurationChanged = true;
        } else if (keyEvent.getKeyChar() == '+') {
            cfg.adjustPosCloudScaleFactor(0.01f);
            configurationChanged = true;
        } else if (keyEvent.getKeyChar() == '-') {
            cfg.adjustPosCloudScaleFactor(-0.01f);
            configurationChanged = true;
        }
    }

    @Override
    public void keyReleased() {
        if (configurationChanged) {
            try {
                cfg.persistProjectionProperties();
                configurationChanged = false;
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    private static void initPlaylistManager(PlaylistManager pPlaylistManager, Configuration pCfg) {

		LOG.info("Initializing playlist manager ...");

        try {
            pPlaylistManager.init(pCfg);
        } catch (NoPlaylistSelectedException e) {
            LOG.error(e,e);
            System.exit(0);
        } catch (PlaylistParserException e) {
            LOG.error(e,e);
        }

	}


}
